package com.neo.tiny.oauth.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.neo.tiny.common.exception.WebApiException;
import com.neo.tiny.oauth.enums.LogoutScope;
import com.neo.tiny.token.store.AccessTokenInfo;
import com.neo.tiny.token.store.OAuth2AccessToken;
import com.neo.tiny.token.store.TokenStore;
import com.neo.tiny.oauth.entity.SysOauth2AccessTokenDO;
import com.neo.tiny.oauth.entity.SysOauth2ClientDO;
import com.neo.tiny.oauth.entity.SysOauth2RefreshTokenDO;
import com.neo.tiny.oauth.mapper.SysOauth2AccessTokenMapper;
import com.neo.tiny.oauth.mapper.SysOauth2RefreshTokenMapper;
import com.neo.tiny.oauth.service.Oauth2TokenService;
import com.neo.tiny.oauth.service.SysOauth2ClientService;
import com.neo.tiny.oauth.vo.token.Oauth2AccessTokenPageReqVO;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author yqz
 * @Description 授权相关业务
 * @CreateDate 2022/11/11 9:32
 */
@Service
@AllArgsConstructor
public class Oauth2TokenServiceImpl implements Oauth2TokenService {


    private final SysOauth2ClientService oauth2ClientService;

    private final SysOauth2RefreshTokenMapper oauth2RefreshTokenMapper;

    private final SysOauth2AccessTokenMapper oauth2AccessTokenMapper;

    private final TokenStore tokenStore;

    @Override
    public OAuth2AccessToken createAccessToken(Long userId, String userName, Integer userType, String clientId, List<String> scopes) {

        // 创建token前先判断当前用户是否已经存在token，如果存在则还返回原来的
        OAuth2AccessToken oAuth2AccessToken = tokenStore.getAccessTokenWithUserInfo(clientId, userType, userId);

        if (BeanUtil.isNotEmpty(oAuth2AccessToken)) {
            return oAuth2AccessToken;
        }

        SysOauth2ClientDO oauth2ClientDO = oauth2ClientService.validOauthClient(clientId);

        // 创建刷新令牌
        SysOauth2RefreshTokenDO refreshTokenDO = createOauth2RefreshToken(userId, userName, userType, oauth2ClientDO, scopes);

        // 创建令牌
        SysOauth2AccessTokenDO oauth2AccessToken = createOauth2AccessToken(refreshTokenDO, oauth2ClientDO);
        return BeanUtil.copyProperties(oauth2AccessToken, AccessTokenInfo.class);
    }

    @Override
    public OAuth2AccessToken refreshAccessToken(String refreshToken, String clientId) {
        // 查询刷新令牌
        SysOauth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshTokenAndClientId(refreshToken, clientId);
        if (Objects.isNull(refreshTokenDO)) {
            throw new WebApiException("无效的刷新令牌");
        }
        // 校验 client
        if (ObjectUtil.notEqual(refreshTokenDO.getClientId(), clientId)) {
            throw new WebApiException("刷新令牌的客户端编号不正确");
        }
        // 已过期的情况下，删除刷新令牌
        if (refreshTokenDO.getExpiresTime().isBefore(LocalDateTime.now())) {
            oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId());
            throw new WebApiException("刷新令牌已过期");
        }

        List<SysOauth2AccessTokenDO> accessTokenDOList = oauth2AccessTokenMapper.selectListByRefreshTokenAndClientId(refreshToken, clientId);
        if (CollUtil.isNotEmpty(accessTokenDOList)) {
            oauth2AccessTokenMapper.deleteBatchIds(accessTokenDOList.stream().map(SysOauth2AccessTokenDO::getId).collect(Collectors.toSet()));
            accessTokenDOList.forEach(item -> {
                // 刷新token，则只清除该client下的token
                tokenStore.removeAccessToken(BeanUtil.copyProperties(item, AccessTokenInfo.class));
            });
        }

        SysOauth2ClientDO oauthClient = oauth2ClientService.validOauthClient(clientId);
        // 创建访问令牌 TODO 校验旧token，如果存在则直接返回
        SysOauth2AccessTokenDO oauth2AccessToken = createOauth2AccessToken(refreshTokenDO, oauthClient);
        return BeanUtil.copyProperties(oauth2AccessToken, AccessTokenInfo.class);
    }

    @Override
    public OAuth2AccessToken getAccessToken(String accessToken) {
        // 从缓存中取
        OAuth2AccessToken storeAccessToken = tokenStore.getAccessToken(accessToken);
        if (!Objects.isNull(storeAccessToken)) {
            return storeAccessToken;
        }
        // 获取不到，从 MySQL 中获取
        SysOauth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
        // 如果在 MySQL 存在，则往 Redis 中写入
        AccessTokenInfo accessTokenInfo = BeanUtil.copyProperties(accessTokenDO, AccessTokenInfo.class);
        if (accessTokenDO != null && accessTokenDO.getExpiresTime().isAfter(LocalDateTime.now())) {

            tokenStore.storeAccessToken(accessTokenInfo);
        }
        return accessTokenInfo;
    }

    @Override
    public OAuth2AccessToken checkAccessToken(String accessToken) {
        OAuth2AccessToken accessTokenDO = getAccessToken(accessToken);
        if (accessTokenDO == null) {
            throw new WebApiException("访问令牌不存在");

        }
        if (accessTokenDO.getExpiresTime().isBefore(LocalDateTime.now())) {
            throw new WebApiException("访问令牌已过期");
        }
        return accessTokenDO;
    }

    @Override
    public SysOauth2AccessTokenDO removeAccessToken(String accessToken, Integer removeScope) {
        // 删除访问令牌
        SysOauth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
        if (accessTokenDO == null) {
            return null;
        }
        Long userId = accessTokenDO.getUserId();
        // 判断是否为全平台退出，如果是全平台退出，则查询到该accessToken所属用户的所有登录信息，并删除
        List<SysOauth2AccessTokenDO> tokenList = oauth2AccessTokenMapper.selectListByUserIdAndAccessToken(userId,
                LogoutScope.isAllClient(removeScope) ? null : accessToken);

        oauth2AccessTokenMapper.deleteBatchIds(tokenList.stream().map(SysOauth2AccessTokenDO::getId).collect(Collectors.toSet()));
        AccessTokenInfo accessTokenInfo = BeanUtil.copyProperties(accessTokenDO, AccessTokenInfo.class);
        // 如果为全平台退出，则清空该用户的所有token缓存
        if (LogoutScope.isAllClient(removeScope)) {
            accessTokenInfo.setClientId(null);
        }
        tokenStore.removeAccessToken(accessTokenInfo);
        // 删除刷新令牌
        oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken());
        return accessTokenDO;
    }


    @Override
    public IPage<SysOauth2AccessTokenDO> getAccessTokenPage(Oauth2AccessTokenPageReqVO reqVO) {
        return oauth2AccessTokenMapper.selectPage(reqVO);

    }

    private SysOauth2AccessTokenDO createOauth2AccessToken(SysOauth2RefreshTokenDO refreshTokenDO, SysOauth2ClientDO clientDO) {
        SysOauth2AccessTokenDO accessTokenDO = new SysOauth2AccessTokenDO();
        accessTokenDO.setAccessToken(generateAccessToken());
        accessTokenDO.setUserId(refreshTokenDO.getUserId());
        accessTokenDO.setUserName(refreshTokenDO.getUserName());
        accessTokenDO.setUserType(refreshTokenDO.getUserType());
        accessTokenDO.setClientId(clientDO.getClientId());
        accessTokenDO.setScopes(refreshTokenDO.getScopes());
        accessTokenDO.setRefreshToken(refreshTokenDO.getRefreshToken());
        accessTokenDO.setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds()));
        oauth2AccessTokenMapper.insert(accessTokenDO);

        // 放入缓存
        tokenStore.storeAccessToken(BeanUtil.copyProperties(accessTokenDO, AccessTokenInfo.class));
        // 记录到 Redis 中
        return accessTokenDO;
    }

    private SysOauth2RefreshTokenDO createOauth2RefreshToken(Long userId, String userName, Integer userType, SysOauth2ClientDO clientDO, List<String> scopes) {
        SysOauth2RefreshTokenDO refreshToken = new SysOauth2RefreshTokenDO();
        refreshToken.setRefreshToken(generateRefreshToken());
        refreshToken.setUserId(userId);
        refreshToken.setUserName(userName);
        refreshToken.setUserType(userType);
        refreshToken.setClientId(clientDO.getClientId());
        refreshToken.setScopes(scopes);
        refreshToken.setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds()));
        oauth2RefreshTokenMapper.insert(refreshToken);
        return refreshToken;
    }

    private static String generateAccessToken() {
        return IdUtil.fastSimpleUUID();
    }

    private static String generateRefreshToken() {
        return IdUtil.fastSimpleUUID();
    }

}
